/*
** DLNA ContentDirectory service
*/

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <inttypes.h>

#include <http_protocol.h>
#include <http_log.h>
#include <http_core.h>
#include <http_request.h>
#include <util_xml.h>

#include <apr_pools.h>
#include <apr_strings.h>
#include <apr_file_io.h>
#include <apr_file_info.h>
#include <apr_xml.h>

#include "extattr.h"
#include "defs.h"
#include "template.h"
#include "xml.h"
#include "content.h"

#define SERVICE_CONTENT_DIRECTORY  "urn:schemas-upnp-org:service:ContentDirectory:1"
#define CONTENT_ACTION_BROWSE "#Browse"
#define CONTENT_ACTION_SORTCAP "#GetSortCapabilities"


enum sort_t{
	SORT_TITLE_ASC,
	SORT_TITLE_DESC,
	SORT_DATE_ASC,
	SORT_DATE_DESC,

	SORT_UNSUPPORTED
};

struct ent {
	apr_finfo_t finfo;

	struct exattr exinfo;
};

static int
cmp_date(const struct ent*e1, const struct ent*e2)
{
#if 1
	if (e1->finfo.mtime == e2->finfo.mtime)
		return 0;
	else if (e1->finfo.mtime < e2->finfo.mtime)
		return -1;
	return 1;
#else
	if (e1->exinfo.dc_date == NULL) {
		if (e2->exinfo.dc_date == NULL)
			return 0;
		else
			return 1;
	} else {
		if (e2->exinfo.dc_date == NULL)
			return -1;
	}

	return strcmp(e1->exinfo.dc_date, e2->exinfo.dc_date);
#endif
}
static int
cmp_title(const struct ent*e1, const struct ent*e2)
{
	// return strcmp(e1->finfo.name, e2->finfo.name);
	return strcmp(e1->exinfo.dc_title, e2->exinfo.dc_title);
}

static int
cmp_base(void* arg, const void *v1, const void *v2)
{
	apr_array_header_t* criteria = (apr_array_header_t*) arg;

	const struct ent* e1 = *(const struct ent**)v1;
	const struct ent* e2 = *(const struct ent**)v2;

	int i = 0;
	for (i = 0; i < criteria->nelts; ++i) {
		enum sort_t sc = ((enum sort_t*)(criteria->elts))[i];

		int result = 0;
		if (sc == SORT_TITLE_ASC)
			result = cmp_title(e1, e2);
		else if (sc == SORT_TITLE_DESC)
			result = cmp_title(e2, e1);
		else if (sc == SORT_DATE_ASC)
			result = cmp_date(e1, e2);
		else if (sc == SORT_DATE_DESC)
			result = cmp_date(e2, e1);

		if (result != 0)
			return result;
	}

	return 0;
}

static apr_array_header_t*
get_sorted_dirent(apr_pool_t* pool, const char* dpath, apr_array_header_t* sortCriteria)
{
	apr_status_t status;
	apr_dir_t* d;

	status = apr_dir_open(&d, dpath, pool);
	if (status != APR_SUCCESS)
		return NULL;

	// --------
	apr_array_header_t* darr = apr_array_make(pool, 16, sizeof(struct ent*));

	do {
		struct ent* e = (struct ent*) apr_palloc(pool, sizeof(struct ent));
		status = apr_dir_read(&e->finfo, APR_FINFO_NORM, d);
		if (status != APR_SUCCESS) {
			break;
		}

		if (APR_STATUS_IS_INCOMPLETE(status)) {
			continue; /* ignore un-stat()able files */
		} else if (e->finfo.name[0] == '.') {
			continue;
		}

		char* fpath = apr_pstrcat(pool, dpath, "/", e->finfo.name, NULL);
		get_extended_attribute(pool, &e->exinfo, fpath);
		if (e->exinfo.dc_title != NULL)
			*(struct ent**)apr_array_push(darr) = e;
	} while (1);

	// sort array
	qsort_r(darr->elts, darr->nelts, darr->elt_size,
	    sortCriteria, cmp_base);

	apr_dir_close(d);

	return darr;
}

struct browse_args {
	const char* objectId;
	apr_table_t* filter;
	int startingIndex;
	int requestedCount;
	apr_array_header_t* sortCriteria;
};

static enum sort_t
get_sortkey(const char* key)
{
	if (strcmp(key, "+dc:title") == 0)
		return SORT_TITLE_ASC;
	if (strcmp(key, "-dc:title") == 0)
		return SORT_TITLE_DESC;
	if (strcmp(key, "+dc:date") == 0)
		return SORT_DATE_ASC;
	if (strcmp(key, "-dc:date") == 0)
		return SORT_DATE_DESC;

	return SORT_UNSUPPORTED;
}

static struct browse_args*
create_bargs(request_rec *r, apr_xml_doc* pdoc)
{
	struct browse_args* bargs = (struct browse_args*)apr_palloc(r->pool, sizeof(struct browse_args));
	apr_array_header_t* earr;
	apr_xml_elem* e;
	const char* data;
	const char* key;
	char* last;

	earr = getElementsByTagName(r->pool, pdoc, "ObjectID");
	if (earr != NULL) {
		e = ((apr_xml_elem **)(earr->elts))[0];
		bargs->objectId = text(e, r->pool);
	} else {
		bargs->objectId = NULL;
	}

	earr = getElementsByTagName(r->pool, pdoc, "Filter");
	if (earr != NULL) {
		e = ((apr_xml_elem **)(earr->elts))[0];
		data = text(e, r->pool);

		bargs->filter = apr_table_make(r->pool, 4);
		key = apr_strtok(data, ",", &last);
		while (key != NULL) {
			apr_table_set(bargs->filter, key, NULL);
			key = apr_strtok(NULL, ",", &last);
		}
	} else {
		bargs->filter = NULL;
	}

	earr = getElementsByTagName(r->pool, pdoc, "StartingIndex");
	if (earr != NULL) {
		e = ((apr_xml_elem **)(earr->elts))[0];
		bargs->startingIndex = atoi(text(e, r->pool));
	} else {
		bargs->startingIndex = 0;
	}

	earr = getElementsByTagName(r->pool, pdoc, "RequestedCount");
	if (earr != NULL) {
		e = ((apr_xml_elem **)(earr->elts))[0];
		bargs->requestedCount = atoi(text(e, r->pool));
	} else {
		bargs->requestedCount = 10;
	}

	earr = getElementsByTagName(r->pool, pdoc, "SortCriteria");
	if (earr != NULL) {
		e = ((apr_xml_elem **)(earr->elts))[0];
		data = text(e, r->pool);

		bargs->sortCriteria = apr_array_make(r->pool, 4, sizeof(enum sort_t));
		key = apr_strtok(data, ",", &last);
		while (key != NULL) {
			enum sort_t sc = get_sortkey(key);
			if (sc != SORT_UNSUPPORTED)
				*(enum sort_t*)apr_array_push(bargs->sortCriteria) = sc;
			key = apr_strtok(NULL, ",", &last);
		}
	} else {
		bargs->sortCriteria = NULL;
	}

	return bargs;
}

static void
browse_folder(request_rec* r, apr_array_header_t* darr, struct browse_args* bargs)
{
	ap_set_content_type(r, "text/xml; charset=utf-8");

	ap_rputs(template_browse_1,
	    r);

	int numberReturned = 0;
	int i;
	for (i = bargs->startingIndex;
	    i < (bargs->startingIndex + bargs->requestedCount) && i < darr->nelts;
	    ++i) {
		struct ent* p = APR_ARRAY_IDX(darr, i, struct ent*);

		if (p->finfo.filetype == APR_DIR) {
			ap_rprintf(r, template_browse_2,
			    (strcmp(bargs->objectId, "0") == 0)
			    ? ap_escape_html(r->pool, MODULE_ROOT)
			    : ap_escape_html(r->pool, bargs->objectId),
			    ap_escape_html(r->pool, p->finfo.name),

			    ap_escape_html(r->pool, bargs->objectId),
			    double_escape(r->pool, p->exinfo.dc_title));
		} else if (p->finfo.filetype == APR_REG) {
			ap_rprintf(r, template_browse_3,
			    (strcmp(bargs->objectId, "0") == 0)
			    ? ap_escape_html(r->pool, MODULE_ROOT)
			    : ap_escape_html(r->pool, bargs->objectId),
			    ap_escape_html(r->pool, p->finfo.name),

			    ap_escape_html(r->pool, bargs->objectId),
			    double_escape(r->pool, p->exinfo.dc_title),
			    double_escape(r->pool, p->exinfo.dc_date),
			    apr_off_t_toa(r->pool, p->finfo.size),
			    ap_escape_html(r->pool, p->exinfo.duration),
			    ap_escape_html(r->pool, p->exinfo.bitrate),
			    ap_escape_html(r->pool, p->exinfo.resolution),

			    (strcmp(bargs->objectId, "0") == 0)
			    ? ap_construct_url(r->pool, MODULE_ROOT, r)
			    : ap_construct_url(r->pool, bargs->objectId, r),

			    ap_escape_uri(r->pool, p->finfo.name));
		} else {
			ap_log_error(APLOG_MARK, APLOG_ERR|APLOG_NOERRNO,
			    HTTP_INTERNAL_SERVER_ERROR, r->server,
			    "parentId: %s, name: %s, filetype=%d: non regular file\n",
			    bargs->objectId,
			    p->finfo.name,
			    p->finfo.filetype);
		}

		++numberReturned;
	}

	ap_rprintf(r, template_browse_4,
	    numberReturned,
	    darr->nelts);
}

static char*
oid_to_path(const char* oid, request_rec* r, apr_finfo_t* last_finfo)
{
	if (oid == NULL)
		return NULL;
	if (! strcmp(oid, "0"))
		oid = MODULE_ROOT;
	if (oid[0] != '/')
		return NULL;

	request_rec* subr = ap_sub_req_lookup_uri(oid, r, NULL);

	*last_finfo = subr->finfo;
	return apr_pstrdup(r->pool, subr->filename);
}

static int
content_browse(request_rec *r)
{
	apr_xml_doc* pdoc = NULL;
	int st = ap_xml_parse_input(r, &pdoc);
	if (st != OK) {
		ap_log_error(APLOG_MARK, APLOG_ERR, st, r->server,
		    "SOAP body required, XML Error (%d)",
		    st);

		return HTTP_INTERNAL_SERVER_ERROR;
	}

	struct browse_args* bargs = create_bargs(r, pdoc);
	apr_finfo_t finfo;
	char* path = oid_to_path(bargs->objectId, r, &finfo);
	if (path == NULL) {
		ap_log_error(APLOG_MARK, APLOG_ERR|APLOG_NOERRNO,
		    HTTP_NOT_FOUND, r->server,
		    "ObjectID path not found: %s",
		    bargs->objectId ? bargs->objectId : "(null)");

		return HTTP_INTERNAL_SERVER_ERROR;
	}

	if (finfo.filetype != APR_DIR) {
		ap_log_error(APLOG_MARK, APLOG_ERR|APLOG_NOERRNO, 0, r->server,
		    "ObjectID path not directory: %s",
		    path);

		return HTTP_INTERNAL_SERVER_ERROR;
	}

	apr_array_header_t* darr = get_sorted_dirent(r->pool, path, bargs->sortCriteria);
	if (darr == NULL) {
		ap_log_error(APLOG_MARK, APLOG_ERR|APLOG_NOERRNO, 0, r->server,
		    "cannot browse ObjectID directory: %s",
		    path);

		return HTTP_INTERNAL_SERVER_ERROR;
	}

	browse_folder(r, darr, bargs);

	return OK;
}

static int
content_sortcap(request_rec *r)
{
	ap_set_content_type(r, "text/xml; charset=utf-8");

	ap_rputs(template_sortcap,
	    r);

	return OK;
}

int
dlna_content(request_rec *r)
{
	const char* action = (const char*)apr_table_get(r->headers_in, "SOAPACTION");
	if (action != NULL) {
		while (*action == '\"' || *action == '\'')
			++action;

		if (! strncmp(action, SERVICE_CONTENT_DIRECTORY, strlen(SERVICE_CONTENT_DIRECTORY))) {
			action += strlen(SERVICE_CONTENT_DIRECTORY);
			if (! strncmp(action, CONTENT_ACTION_BROWSE, strlen(CONTENT_ACTION_BROWSE)))
				return content_browse(r);
			else if (! strncmp(action, CONTENT_ACTION_SORTCAP, strlen(CONTENT_ACTION_SORTCAP)))
				return content_sortcap(r);
		}
	}

	action = (const char*)apr_table_get(r->headers_in, "SOAPACTION");
	ap_log_error(APLOG_MARK, APLOG_ERR|APLOG_NOERRNO, 0, r->server,
	    "Cannot parse SOAPACTION: %s",
	    action);

	return HTTP_INTERNAL_SERVER_ERROR;
}
